package tcpserver;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;

public class RFIDServer {
	
	private final RFIDServerConfig config;

	private final AtomicBoolean runningStatus = new AtomicBoolean(true);

	private final Timer delayedQueue = new Timer();

	private final AsynchronousServerSocketChannel channel;
	
	private final ConcurrentHashMap<String, RFIDConnection> connections;
	
	private RFIDServer(SocketAddress address, RFIDServerConfig config) throws IOException { 
		
		this.config = config;
		
		this.channel = defaultChannel(address, config);
		
		this.connections = new ConcurrentHashMap<>();
	}

	private AsynchronousServerSocketChannel defaultChannel(SocketAddress address, RFIDServerConfig config) throws IOException {
		
		AsynchronousServerSocketChannel channel = AsynchronousServerSocketChannel.open();
		
		channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
		
		channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
		
		channel.bind(address, config.getBacklog());
		
		return channel;
	}
	
	public static RFIDServer newLocalServer(int port, RFIDServerConfig config) throws IOException {
		
		return new RFIDServer(new InetSocketAddress(InetAddress.getLocalHost(), port), config);
	}
	
	private RFIDConnection mergeConnections(RFIDConnection connection) {

        return connections.putIfAbsent(connection.getIdentity(), connection) == null ? connection : null;
	}
	
	private boolean isRunning() {
		
		return runningStatus.get();
	}
	
	public void start() {
		
		channel.accept(null, new CallBackForAccept());
	}
	
	public Set<String> getConnectionIdentities() {
		
		return connections.keySet();
	}
	
	public void submit(String key, byte[] frame) throws InterruptedException, ExecutionException {
		
		RFIDConnection connection = connections.get(key);

		if(connection == null) return;

		connection.asyncWrite(frame, new CallBackForWrite());
	}

	private class CallBackForAccept implements CompletionHandler<AsynchronousSocketChannel, Void> {

		@Override public void completed(AsynchronousSocketChannel acceptChannel, Void ignore) {

		    if(isRunning()) delayedQueue.schedule(new TimerTaskForDelayedAccept(), config.getAcceptDelay());

            String acceptChannelIdentity = identityOf(acceptChannel);

            if(acceptChannelIdentity == null) return;

            RFIDConnection connection = mergeConnections(new RFIDConnection(acceptChannelIdentity, acceptChannel, config.getBufferSize()));

            if(connection != null) delayedQueue.schedule(new TimerTaskForDelayedRead(connection), config.getReadDelay());
		}

		@Override public void failed(Throwable exc, Void ignore) {

            if(isRunning()) delayedQueue.schedule(new TimerTaskForDelayedAccept(), config.getAcceptDelay());
        }

		private String identityOf(AsynchronousSocketChannel acceptChannel) {

            try {

                return acceptChannel.getRemoteAddress().toString();

            } catch (IOException ignored) {

                return null;
            }
        }
	}

    private class TimerTaskForDelayedAccept extends TimerTask {

        @Override public void run() {

            if(isRunning()) channel.accept(null, new CallBackForAccept());
        }
    }

	private class CallBackForRead implements CompletionHandler<Integer, RFIDConnection> {
		
		@Override public void completed(Integer length, RFIDConnection connection) {
			
			connection.flush();

			if(isRunning()) delayedQueue.schedule(new TimerTaskForDelayedRead(connection), config.getReadDelay());
		}

		@Override public void failed(Throwable exc, RFIDConnection connection) {

		    connections.remove(connection.getIdentity());
		}
	}

    private class TimerTaskForDelayedRead extends TimerTask {

        private final RFIDConnection connection;

        TimerTaskForDelayedRead(RFIDConnection connection) {

            this.connection = connection;
        }

        @Override public void run() {

            if(isRunning()) connection.asyncRead(new CallBackForRead());
        }
    }

    private class CallBackForWrite implements CompletionHandler<Integer, RFIDConnection> {
		
		@Override public void completed(Integer length, RFIDConnection connection) {


		}

		@Override public void failed(Throwable exc, RFIDConnection connection) {

            connections.remove(connection.getIdentity());
        }
	}
}
